﻿using SignalBox.Core;
using SignalBox.Core.Model;
using SignalBox.Core.Model.Graph;
using SignalBox.Core.Model.Timetable;
using SignalBox.Core.UI;
using SignalBox.Core.Util;
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using System.Runtime.InteropServices;

namespace SignalBoxSimulator
{
    public class SimulationController
    {

        private TrackLayout _trackLayout;
        private TrainList _trainList;
        private TrainDetails _trainDetails;
        private Alerts _alerts;
        private LayoutPanel _layoutPanel;

        public delegate void UpdateStatusHandler(string message);
        public event UpdateStatusHandler UpdateStatus;
        public event UpdateStatusHandler UpdateCoords;
        public delegate void UpdateDataHandler();
        public event UpdateDataHandler UpdateData;

        private Timetable _timetable;

        private DateTime currentTime;
        private DateTime previousTime;

        private Timer timer;

        private List<Train> trains;

        public SimulationController(TrackLayout trackLayout, TrainList trainList, TrainDetails trainDetails, Alerts alerts, LayoutPanel layoutPanel)
        {
            _trackLayout = trackLayout;
            _trainList = trainList;
            _trainDetails = trainDetails;
            _alerts = alerts;
            _layoutPanel = layoutPanel;


            _layoutPanel.MouseUp += _layoutPanel_MouseUp;
            _layoutPanel.MouseMove += _layoutPanel_MouseMove;

            Speed = 1;
        }

        public int Speed
        {
            get;
            set;
        }

        public List<Train> Trains
        {
            get { return trains; }
        }

        public DateTime CurrentTime
        {
            get { return currentTime; }
        }

        public void SetTimeTable(Timetable tt)
        {
            _timetable = tt;

            currentTime = GetDateTime(_timetable.StartTime);
            previousTime = currentTime;

            CreateTrains();
        }

        private DateTime GetDateTime(string time)
        {
            string[] parts = time.Split(":".ToCharArray());
            int hour = Convert.ToInt32(parts[0]);
            int minutes = Convert.ToInt32(parts[1]);

            DateTime result = DateTime.Now.Date;
            result = result.AddHours(hour);
            result = result.AddMinutes(minutes);

            return result;
        }

        private void CreateTrains()
        {

            trains = new List<Train>();
            foreach (TrainSchedule ts in _timetable.TrainSchedules)
            {
                Train t = new Train();
                t.TrainCode = ts.TrainCode;
                t.MaxSpeed = ts.TrainType.MaxSpeed;

                foreach (TrainScheduleAction ac in ts.ScheduleActions)
                {
                    TrainStop stop = new TrainStop();
                    if (ac is EnterLayoutAction)
                    {

                        stop.Action = TrainStop.TrainAction.ENTER;
                        stop.Location = ((EnterLayoutAction)ac).Location;
                    }
                    else if (ac is ExitLayoutAction)
                    {
                        stop.Action = TrainStop.TrainAction.EXIT;
                        stop.Location = ((ExitLayoutAction)ac).Location;
                    }
                    else if (ac is StopAtLocationAction)
                    {
                        stop.Action = TrainStop.TrainAction.STOP;
                        stop.Location = ((StopAtLocationAction)ac).Location;
                        StopAtLocationAction sala = (StopAtLocationAction)ac;
                        stop.Depart = GetDateTime(sala.DepartureTime);
                        stop.WaitTime = sala.WaitTime;

                    }


                    stop.Arrive = GetDateTime(ac.Time);
                    t.Stops.Add(stop);

                }

                trains.Add(t);


            }

            _layoutPanel.Trains = trains;
        }

        void _layoutPanel_MouseMove(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            Point coords = _layoutPanel.GetCell(new Point(e.X, e.Y));

            string extraInfo = "";

            TrackCell tc = _trackLayout.getCell(coords.X, coords.Y);
            if (tc != null)
            {
                extraInfo += " - Length: " + tc.Length.ToString() + "m";
                if (tc.Name != "")
                    extraInfo += " - " + tc.Name;
                if (tc.Platform != "")
                    extraInfo += "@" + tc.Platform;
            }

            SetCoords(String.Format("X: {0} - Y: {1}", coords.X, coords.Y) + extraInfo);
        }

        void _layoutPanel_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
        {
            Point coords = _layoutPanel.GetCell(new Point(e.X, e.Y));
            TrackCell cell = _trackLayout.getCell(coords.X, coords.Y);

            if (cell != null)
            {

                if (cell.TrackCellType.Category == "SWITCH")
                {
                    if (!cell.Locked)
                    {
                        cell.State += 1;
                        if (cell.State == 3)
                            cell.State = 1;
                    }
                    _layoutPanel.Invalidate();
                }
                else if (cell.TrackCellType.Category == "SIGNAL")
                {
                    ToggleSignalLock(cell);
                    _layoutPanel.Invalidate();
                }
            }

        }


        private List<TrackCell> route;

        private void ToggleSignalLock(TrackCell tc)
        {

            route = RouteFinding.FindRouteFromSignal(_trackLayout, tc);
            if (route.Count > 0)
            {

                foreach (TrackCell track in route)
                {
                    // if track is occupied by train we cannot change signal
                    if (track.OccupiedBy != null && track.TrackCellType.Category != "SIGNAL")
                        return;
                }

                foreach (TrackCell track in route)
                {
                    track.Locked = !track.Locked;

                    if (track.TrackCellType.Category == "CROSS")
                    {
                        track.State = track.RouteFindingState;
                    }

                }
            }

            RouteFinding.UpdateSignals(_trackLayout);

        }


        private void SetCoords(string message)
        {
            if (UpdateCoords != null)
                UpdateCoords(message);
        }

        private void SetStatusMessage(string message)
        {
            if (UpdateStatus != null)
                UpdateStatus(message);
        }

        public bool IsRunning
        {
            get
            {
                if (timer == null) return false;
                return timer.Enabled;
            }
        }

        public void StartSimulation()
        {
            if (_timetable == null) return;

            timer = new Timer();
            timer.Interval = 1000;
            timer.Tick += timer_Tick;
            timer.Enabled = true;
        }

        public void StopSimulation()
        {
            if (timer == null) return;

            timer.Enabled = false;
        }

        void timer_Tick(object sender, EventArgs e)
        {
            //TODO: change this for speed
            currentTime = currentTime.AddSeconds(Speed);
            UpdateTrains();

            _layoutPanel.Invalidate();

            if (UpdateData != null)
                UpdateData();

            previousTime = currentTime;

        }

        private void UpdateTrains()
        {

            int seconds = (int)currentTime.Subtract(previousTime).TotalSeconds;

            for (int s = 0; s < seconds; s++)
            {

                foreach (Train t in trains)
                {
                    if (t.State == Train.TrainState.NOT_AVAILABLE || t.State == Train.TrainState.DELAYED_ENTRY)
                    {
                        if (t.Stops[0].Arrive <= previousTime)
                        {
                            EnterTrainOnLayout(t);
                        }


                    }
                    else if (t.State == Train.TrainState.STOPPED)
                    {
                        if (t.NextStop.ActualDeparture <= previousTime)
                        {
                            t.NextStop.Passed = true;
                            t.State = Train.TrainState.ACCELERATING;
                            t.Acceleration = t.AcceleratingSpeed;
                        }
                    }
                    else if (t.State == Train.TrainState.WAITING_SIGNAL)
                    {
                        CheckWaitSignal(t);
                    }

                    if (t.State == Train.TrainState.RUNNING || t.State == Train.TrainState.ACCELERATING || t.State == Train.TrainState.BRAKING)
                    {


                        AdvancePosition(t);
                    }

                }

                previousTime = previousTime.AddSeconds(1);
            }

        }

        private void AdvancePosition(Train t)
        {

            TrackCell tc = _trackLayout.getCell(t.HeadLocation.X, t.HeadLocation.Y);




            double travelled = t.CurrentSpeed / 3.6 + t.Acceleration / 2;
            double travelled2 = travelled;

            while (t.DistanceTravelledCurrentCell + travelled > tc.Length)
            {

                if (tc.TrackCellType.Category == "SIGNAL" && !tc.Locked)
                {
                    t.CurrentSpeed = 0;
                    t.State = Train.TrainState.WAITING_SIGNAL;
                    t.DistanceTravelledCurrentCell = tc.Length;
                    t.SignalWaitStart = previousTime;
                    return;
                }

                if (tc.Name != String.Empty && t.NextStop.Location.IndexOf(tc.Name) >= 0 && tc.TrackCellType.Category != "ENTRYEXIT")
                {
                    t.CurrentSpeed = 0;
                    t.State = Train.TrainState.STOPPED;
                    t.DistanceTravelledCurrentCell = tc.Length;
                    t.NextStop.ActualArrival = previousTime;
                    if (t.NextStop.WaitTime > 0 && t.NextStop.ActualArrival.AddSeconds(t.NextStop.WaitTime) > t.NextStop.Depart)
                        t.NextStop.ActualDeparture = t.NextStop.ActualArrival.AddSeconds(t.NextStop.WaitTime);
                    else
                        t.NextStop.ActualDeparture = t.NextStop.Depart;

                    return;
                }

                travelled -= ((double)tc.Length - t.DistanceTravelledCurrentCell);


                TrackCell prevCell = tc;
                tc = MoveTrainToNextCell(t);

                if (prevCell == tc)
                    break;
                //CheckNeedsToBrake(t);
            }

            t.DistanceTravelledCurrentCell += travelled;



            CheckNeedsToBrake(t);
            UpdateTrainTail(t);
            //CalculateNewSpeed(t, travelled2);


        }

        private void UpdateTrainTail(Train t)
        {
            double lengthRemaining = t.Length - t.DistanceTravelledCurrentCell;

            TrackCell currCell = _trackLayout.getCell(t.HeadLocation.X, t.HeadLocation.Y);

            if (lengthRemaining < 0 && currCell.TrackCellType.Category == "ENTRYEXIT" && currCell.TrackCellType.GetOutputDirectionForEntry() != t.CurrentDirection)
            {
                t.State = Train.TrainState.LEFT;
                for (int j = t.MovementHistory.Count - 1; j >= 0; j--)
                {
                    if (t.MovementHistory[j].Occupied)
                    {
                        t.MovementHistory[j].Occupied = false;
                        t.MovementHistory[j].Locked = false;
                        t.MovementHistory[j].OccupiedBy = null;
                        if (t.MovementHistory[j].TrackCellType.Category == "SIGNAL")
                            RouteFinding.UpdateSignals(_trackLayout);
                    }
                    else
                    {
                        break;
                    }
                }
                return;
            }


            t.MovementHistory[t.MovementHistory.Count - 1].Occupied = true;
            if (t.MovementHistory.Count > 1)
            {

                for (int j = t.MovementHistory.Count - 2; j >= 0; j--)
                {
                    if (lengthRemaining < 0 && t.MovementHistory[j].Occupied)
                    {
                        t.MovementHistory[j].Occupied = false;
                        t.MovementHistory[j].Locked = false;
                        t.MovementHistory[j].OccupiedBy = null;
                        if (t.MovementHistory[j].TrackCellType.Category == "SIGNAL")
                            RouteFinding.UpdateSignals(_trackLayout);

                    }
                    else if (lengthRemaining < 0 && !t.MovementHistory[j].Occupied)
                        break;
                    else
                    {
                        t.MovementHistory[j].Occupied = true;
                        t.MovementHistory[j].OccupiedBy = t;
                    }
                    lengthRemaining -= t.MovementHistory[j].Length;
                }
            }

        }

        private TrackCell MoveTrainToNextCell(Train t)
        {

            TrackCell currCell = _trackLayout.getCell(t.HeadLocation.X, t.HeadLocation.Y);
            /*if (currCell.Locked)
            {
                currCell.Locked = false;

                if (currCell.TrackCellType.Category == "SIGNAL")
                {
                    RouteFinding.UpdateSignals(_trackLayout);
                }
            }*/

            currCell.OccupiedBy = null;

            Point p = RouteFinding.GetNextCell(t.HeadLocation, t.CurrentDirection);
            DIRECTION dIn = RouteFinding.GetOppositeDirection(t.CurrentDirection);

            TrackCell tc = _trackLayout.getCell(p.X, p.Y);

            if (tc == null && currCell.TrackCellType.Category == "ENTRYEXIT")
            {
                tc = currCell;
            }
            else
            {
                t.DistanceTravelledCurrentCell = 0;

                DIRECTION dOut = tc.TrackCellType.GetOutputDirectionForInput(dIn, tc.State);
                if (dOut == DIRECTION.INVALID)
                    throw new Exception("Invalid direction");
                t.CurrentDirection = dOut;
                t.HeadLocation = tc.Location;
                t.MovementHistory.Add(tc);

                if (tc.GetMaxSpeed(dOut) > 0)
                    t.CurrentSpeedRestriction = tc.GetMaxSpeed(dOut);

                Console.WriteLine("New location for train " + t.TrainCode + " (" + t.HeadLocation.X.ToString() + " - " + t.HeadLocation.Y.ToString() + ")");

                tc.OccupiedBy = t;
            }
            return tc;
        }



        private void CheckNeedsToBrake(Train t)
        {

            double max = CalculateMaxSpeed(t);

            if (t.State == Train.TrainState.RUNNING)
            {
                if (t.CurrentSpeed > max)
                {
                    t.State = Train.TrainState.BRAKING;
                    t.Acceleration = -t.BrakingSpeed;
                }
                else if (t.CurrentSpeed < max)
                {
                    t.State = Train.TrainState.ACCELERATING;
                    t.Acceleration = t.AcceleratingSpeed;
                }
            }
            else
            {
                t.CurrentSpeed += (t.Acceleration) * 3.6;
            }
            if (t.State == Train.TrainState.ACCELERATING)
            {
                if (t.CurrentSpeed > max)
                {
                    t.CurrentSpeed = max;
                    t.State = Train.TrainState.RUNNING;
                }
            }
            else if (t.State == Train.TrainState.BRAKING)
            {
                if (t.CurrentSpeed < 0)
                {
                    t.CurrentSpeed = 0;

                    TrackCell tc = _trackLayout.getCell(t.HeadLocation.X, t.HeadLocation.Y);

                    if (tc.TrackCellType.Category == "SIGNAL" && !tc.Locked)
                    {
                        t.State = Train.TrainState.WAITING_SIGNAL;
                        t.SignalWaitStart = previousTime;
                    }
                    else
                        t.State = Train.TrainState.STOPPED;
                }
                else if (t.CurrentSpeed < max)
                {
                    t.State = Train.TrainState.ACCELERATING;
                    t.Acceleration = t.AcceleratingSpeed;
                }
            }

            /*if (t.CurrentSpeed > max && t.State == Train.TrainState.RUNNING)
            {
                t.Acceleration = -t.BrakingSpeed;
                t.State = Train.TrainState.BRAKING;
            }
            else if (t.CurrentSpeed < max && t.State != Train.TrainState.ACCELERATING)
            {
                t.State = Train.TrainState.ACCELERATING;
                t.Acceleration = t.AcceleratingSpeed;
            }
            else
            {
                if (t.State != Train.TrainState.ACCELERATING)
                {
                    t.State = Train.TrainState.ACCELERATING;
                    t.Acceleration = t.AcceleratingSpeed;
                }
                t.CurrentSpeed += (t.AcceleratingSpeed / 2) * 3.6;
            }*/
        }

        private double CalculateMaxSpeed(Train t)
        {
            TrackCell tc = _trackLayout.getCell(t.HeadLocation.X, t.HeadLocation.Y);

            List<TrackCell> nextRed = RouteFinding.GetRouteToNextRedSignal(_trackLayout, tc, t.CurrentDirection);

            double maxspeedStop = 0;
            double length = 0.0;

            if (nextRed.Count > 0 && nextRed[nextRed.Count - 1].TrackCellType.Category == "ENTRYEXIT")
                maxspeedStop = t.MaxSpeed;
            else
            {

                foreach (TrackCell c in nextRed)
                    length += c.Length;



                length -= t.DistanceTravelledCurrentCell;


                maxspeedStop = MaxApproachSpeed(t, length, 0);

                Console.WriteLine("Distance to next red for train " + t.TrainCode + " at location " + t.HeadLocation + ": " + length.ToString() + " - Current Speed: " + t.CurrentSpeed.ToString() + " - Max Speed: " + maxspeedStop.ToString());
            }

            int maxSpeed = t.MaxSpeed;
            List<TrackCell> nextSpeed = RouteFinding.GetRouteToNextSpeedRestriction(_trackLayout, tc, t.CurrentDirection, out maxSpeed);
            if (maxSpeed == 0) maxSpeed = t.MaxSpeed;

            double maxSpeedRestr = 0.0;

            if (nextSpeed.Count > 1 && nextSpeed[nextSpeed.Count - 1].TrackCellType.Category == "ENTRYEXIT")
                maxSpeedRestr = t.CurrentSpeedRestriction;
            else if (maxSpeed < t.CurrentSpeed)
            {
                length = 0.0;
                foreach (TrackCell c in nextSpeed)
                    length += c.Length;

                length -= t.DistanceTravelledCurrentCell;

                int targetSpeed = maxSpeed;

                maxSpeedRestr = MaxApproachSpeed(t, length, targetSpeed);

            }
            else
            {
                maxSpeedRestr = maxSpeed;
            }

            double max = Math.Min(maxspeedStop, maxSpeedRestr);

            if (t.NextStop != null && t.NextStop.Action == TrainStop.TrainAction.STOP)
            {

                List<TrackCell> nextStation = RouteFinding.GetRouteToNextStop(_trackLayout, tc, t.CurrentDirection, t.NextStop);
                length = 0.0;
                foreach (TrackCell c in nextStation)
                    length += c.Length;

                if (length == 0.0)
                    maxspeedStop = t.MaxSpeed;
                else
                {

                    length -= t.DistanceTravelledCurrentCell;

                    maxspeedStop = MaxApproachSpeed(t, length, 0);
                }
            }

            max = Math.Min(maxspeedStop, max);

            if (max > t.MaxSpeed)
                max = t.MaxSpeed;

            if (max > t.CurrentSpeedRestriction)
                max = t.CurrentSpeedRestriction;

            return max;
        }

        private double MaxApproachSpeed(Train trn, double distance, int targetspeed)
        {

            double s, v0;
            double v;
            double deceleration = -trn.BrakingSpeed + 0.07;

            // v0 = sqrt(v * v - 2 a s)

            s = distance;
            v0 = ((double)targetspeed) / 3.6;

            s = -s * 2.0 * deceleration + v0 * v0;
            v = (Math.Sqrt(s) * 3.6);

            return v;
        }


        private void CalculateNewSpeed(Train t, double travelled)
        {
            double speed = (t.CurrentSpeed / 3.6) * (t.CurrentSpeed / 3.6) + 2 * t.Acceleration * travelled;
            if (speed < 0)
            {
                speed = 0;
            }

            speed = Math.Sqrt(speed) * 3.6;

            if (speed > t.MaxSpeed)
            {
                t.CurrentSpeed = t.MaxSpeed;
                t.State = Train.TrainState.RUNNING;
                t.Acceleration = 0.0;
            }
            else if (speed == 0.0)
            {
                //t.State = Train.TrainState.STOPPED;
                t.Acceleration = 0.0;
            }
            else
            {
                t.CurrentSpeed = speed;
            }
        }

        private void CheckWaitSignal(Train t)
        {
            TrackCell tc = _trackLayout.getCell(t.HeadLocation.X, t.HeadLocation.Y);
            if (tc.Locked)
            {
                t.State = Train.TrainState.ACCELERATING;
                t.Acceleration = t.AcceleratingSpeed;
                tc.Locked = false;
                t.SignalWaitNotified = false;
                MoveTrainToNextCell(t);
            }
            else
            {
                if (t.SignalWaitStart.AddSeconds(60) < previousTime && !t.SignalWaitNotified)
                {
                    _alerts.AppendAlert("Train " + t.TrainCode + " is waiting at signal " + t.HeadLocation.ToString(), previousTime);
                    t.SignalWaitNotified = true;
                }
            }
        }

        private void EnterTrainOnLayout(Train t)
        {

            TrackCell location = _trackLayout.FindLocation(t.Stops[0].Location);

            List<TrackCell> routeToRed = RouteFinding.GetRouteToNextRedSignal(_trackLayout, location, location.TrackCellType.GetOutputDirectionForEntry());

            foreach (TrackCell tc in routeToRed)
            {

                if (tc.OccupiedBy != null)
                {
                    t.State = Train.TrainState.DELAYED_ENTRY;
                    return;
                }

            }


            if (location.TrackCellType.Category == "ENTRYEXIT")
            {
                // enter at entry/exit location
                t.HeadLocation = new Point(location.X, location.Y);
                t.TailLocation = new Point(location.X, location.Y);
                t.MovementHistory.Add(location);
                location.OccupiedBy = t;
                t.CurrentDirection = location.TrackCellType.GetOutputDirectionForEntry();
                t.CurrentSpeedRestriction = t.MaxSpeed;
                t.NextStop.Passed = true;
                // enter train at it's maxspeed (if we need to stop short-term this will be fixed later on while checking the speed)
                double max = CalculateMaxSpeed(t);

                t.CurrentSpeed = max; // t.MaxSpeed > 0 ? t.MaxSpeed : 120;
                t.State = Train.TrainState.RUNNING;
                t.DistanceTravelledCurrentCell = 0;

            }
            else if (location.TrackCellType.Category == "TRACK")
            {
                // enter at station
                t.HeadLocation = new Point(location.X, location.Y);
                t.TailLocation = new Point(location.X, location.Y);
                t.MovementHistory.Add(location);
                t.CurrentSpeed = 0;
                location.OccupiedBy = t;
                t.State = Train.TrainState.STOPPED;
            }



        }

    }
}
